﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Chess
{
    public static class Rules
    {
        public static Status BoardStatus(IReadOnlyBoard board, PieceColor color)
        {
            if (IsInCheckmate(board, color))
            {
                return Status.Checkmate;
            }
            if (IsInCheck(board, color))
            {
                return Status.Check;
            }
            if (IsInStalemate(board, color))
            {
                return Status.Stalemate;
            }
            return Status.Standard;
        }
        public static bool ValidateMove(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            return ValidateGeneralMoveInvariants(board, from, to, turnColor) && ValidatePieceSpecificMoveInvariants(board, from, to, turnColor) && !MoveEndsInSelfCheck(board, from, to, turnColor);
        }
        private static bool ValidateGeneralMoveInvariants(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Func<bool> turnColorIsValid = () => turnColor == (board.History.Any() ? Piece.OppositeColor(board.History.Last().From.Piece.PieceColor) : PieceColor.White);
            Func<bool> pieceExists = () => from.Piece != null;
            Func<bool> pieceIsRightColor = () => from.Piece.PieceColor == turnColor;
            Func<bool> destinationIsNotOwnPiece = () => to.Piece == null || to.Piece.PieceColor != turnColor;
            return turnColorIsValid() && pieceExists() && pieceIsRightColor() && destinationIsNotOwnPiece();
        }
        private static bool ValidatePieceSpecificMoveInvariants(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            switch (from.Piece.PieceType)
            {
                case PieceType.Bishop: { return ValidateBishopMove(board, from, to, turnColor); }
                case PieceType.King: { return ValidateKingMove(board, from, to, turnColor); }
                case PieceType.Knight: { return ValidateKnightMove(board, from, to, turnColor); }
                case PieceType.Queen: { return ValidateQueenMove(board, from, to, turnColor); }
                case PieceType.Pawn: { return ValidatePawnMove(board, from, to, turnColor); }
                case PieceType.Rook: { return ValidateRookMove(board, from, to, turnColor); }
            }
            throw new InvalidOperationException("Unreachable code path.");
        }
        private static bool MoveEndsInSelfCheck(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Move simulatedMove = new Move(board, from, to);
            Func<IEnumerable<Move>> history = () => board.History.Concat(new[] { simulatedMove });
            Func<Column, Row, IReadOnlyTile> getTile = (c, r) => simulatedMove.MutatedTiles.FirstOrDefault(t => t.Position.Column == c && t.Position.Row == r) ?? board.GetTile(c, r);
            Func<PieceColor, IEnumerable<IReadOnlyTile>> tilesByColorOfPiece = c =>
                {
                    IEnumerable<IReadOnlyTile> colorMatchedMutations = simulatedMove.MutatedTiles.Where(t => t.Piece != null && t.Piece.PieceColor == c);
                    return colorMatchedMutations.Concat(board.TilesByColorOfPiece(c).Where(normal => colorMatchedMutations.All(m => !(m.Position.Column == normal.Position.Column && m.Position.Row == normal.Position.Row))));
                };
            return IsInCheck(new MockBoard(history, getTile, tilesByColorOfPiece), turnColor);
        }
        private static bool IsInCheck(IReadOnlyBoard board, PieceColor color)
        {
            IReadOnlyTile kingTile = board.TilesByColorOfPiece(color).First(t => t.Piece.PieceType == PieceType.King);
            return AttackingTiles(board, kingTile, Piece.OppositeColor(color)).Any();
        }
        private static IEnumerable<IReadOnlyTile> AttackingTiles(IReadOnlyBoard board, IReadOnlyTile target, PieceColor attackerColor)
        {
            return board.TilesByColorOfPiece(attackerColor).Where(t => ValidatePieceSpecificMoveInvariants(board, t, target, attackerColor));
        }
        private static bool IsInCheckmate(IReadOnlyBoard board, PieceColor color)
        {
            return IsInCheck(board, color) && !HasValidMove(board, color);
        }
        private static bool IsInStalemate(IReadOnlyBoard board, PieceColor color)
        {
            return !IsInCheck(board, color) && !HasValidMove(board, color);
        }
        private static bool HasValidMove(IReadOnlyBoard board, PieceColor color)
        {
            ICollection<IReadOnlyTile> allTiles = new List<IReadOnlyTile>();
            for (Row row = Row.One; row <= Row.Eight; row++)
            {
                for (Column column = Column.A; column <= Column.H; column++)
                {
                    allTiles.Add(board.GetTile(column, row));
                }
            }
            return board.TilesByColorOfPiece(color).Any(from => allTiles.Any(to => ValidateMove(board, from, to, color)));
        }
        private static bool ValidateBishopMove(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Debug.Assert(from.Piece.PieceType == PieceType.Bishop);
            int columnDifference = to.Position.Column - from.Position.Column;
            int rowDifference = to.Position.Row - from.Position.Row;
            bool diagonal = Math.Abs(columnDifference) == Math.Abs(rowDifference);
            return diagonal && PathIsClear(board, from, to);
        }
        private static bool ValidateKingMove(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Debug.Assert(from.Piece.PieceType == PieceType.King);
            int columnDifference = to.Position.Column - from.Position.Column;
            int rowDifference = to.Position.Row - from.Position.Row;
            bool adjacent = Math.Abs(columnDifference) <= 1 && Math.Abs(rowDifference) <= 1;
            return adjacent || IsValidCastling(board, from, to, turnColor);
        }
        private static bool IsValidCastling(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Debug.Assert(from.Piece.PieceType == PieceType.King);
            int columnDifference = to.Position.Column - from.Position.Column;
            int rowDifference = to.Position.Row - from.Position.Row;
            Column castleColumn = Math.Sign(columnDifference) == 1 ? Column.H : Column.A;
            IReadOnlyTile castleTile = board.GetTile(castleColumn, from.Position.Row);
            Func<bool> attempted = () => Math.Abs(columnDifference) == 2;
            Func<bool> piecesNotMoved = () => !from.PieceHasChanged && !castleTile.PieceHasChanged;
            Func<bool> pathIsSafe = () => PathIsClear(board, from, castleTile) && !IsInCheck(board, turnColor) && !SimpleLineWithoutEndpoints(from.Position, to.Position).Any(p => AttackingTiles(board, board.GetTile(p), Piece.OppositeColor(turnColor)).Any());
            return attempted() && piecesNotMoved() && pathIsSafe();
        }
        private static bool ValidateKnightMove(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Debug.Assert(from.Piece.PieceType == PieceType.Knight);
            int columnDifference = to.Position.Column - from.Position.Column;
            int rowDifference = to.Position.Row - from.Position.Row;
            bool lateral = Math.Abs(columnDifference) == 2 && Math.Abs(rowDifference) == 1;
            bool longitudinal = Math.Abs(rowDifference) == 2 && Math.Abs(columnDifference) == 1;
            return lateral || longitudinal;
        }
        private static bool ValidateQueenMove(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Debug.Assert(from.Piece.PieceType == PieceType.Queen);
            int columnDifference = to.Position.Column - from.Position.Column;
            int rowDifference = to.Position.Row - from.Position.Row;
            bool orthogonal = columnDifference == 0 || rowDifference == 0;
            bool diagonal = Math.Abs(columnDifference) == Math.Abs(rowDifference);
            return (orthogonal || diagonal) && PathIsClear(board, from, to);
        }
        private static bool ValidatePawnMove(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Debug.Assert(from.Piece.PieceType == PieceType.Pawn);
            int columnDifference = to.Position.Column - from.Position.Column;
            int rowDifference = to.Position.Row - from.Position.Row;
            int standardRowDifference = turnColor == PieceColor.White ? 1 : -1;
            int doubleRowDifference = turnColor == PieceColor.White ? 2 : -2;
            Func<bool> standardMove = () => columnDifference == 0 && rowDifference == standardRowDifference && to.Piece == null;
            Func<bool> standardCapture = () => Math.Abs(columnDifference) == 1 && rowDifference == standardRowDifference && to.Piece != null;
            Func<bool> doubleMove = () => !from.PieceHasChanged && columnDifference == 0 && rowDifference == doubleRowDifference && to.Piece == null && PathIsClear(board, from, to);
            return standardMove() || standardCapture() || doubleMove() || IsValidEnPassant(board, from, to, turnColor);
        }
        private static bool IsValidEnPassant(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Debug.Assert(from.Piece.PieceType == PieceType.Pawn);
            int columnDifference = to.Position.Column - from.Position.Column;
            int rowDifference = to.Position.Row - from.Position.Row;
            int validRowDifference = turnColor == PieceColor.White ? 1 : -1;
            bool attempted = Math.Abs(columnDifference) == 1 && rowDifference == validRowDifference && to.Piece == null;
            if (attempted && board.History.Any())
            {
                Move lastMove = board.History.Last();
                bool targetIsValid = lastMove.From.Piece.PieceType == PieceType.Pawn && Math.Abs(lastMove.To.Position.Row - lastMove.From.Position.Row) == 2 && board.GetTile(to.Position.Column, from.Position.Row).Piece == lastMove.From.Piece;
                return targetIsValid;
            }
            return false;
        }
        private static bool ValidateRookMove(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to, PieceColor turnColor)
        {
            Debug.Assert(from.Piece.PieceType == PieceType.Rook);
            int columnDifference = to.Position.Column - from.Position.Column;
            int rowDifference = to.Position.Row - from.Position.Row;
            bool orthogonal = columnDifference == 0 || rowDifference == 0;
            return orthogonal && PathIsClear(board, from, to);
        }
        private static bool PathIsClear(IReadOnlyBoard board, IReadOnlyTile from, IReadOnlyTile to)
        {
            foreach (Position intermediatePosition in SimpleLineWithoutEndpoints(from.Position, to.Position))
            {
                bool pathIsBlocked = board.GetTile(intermediatePosition).Piece != null;
                if (pathIsBlocked)
                {
                    return false;
                }
            }
            return true;
        }
        private static IEnumerable<Position> SimpleLineWithoutEndpoints(Position from, Position to)
        {
            int columnDifference = to.Column - from.Column;
            int rowDifference = to.Row - from.Row;
            bool orthogonal = columnDifference == 0 || rowDifference == 0;
            bool diagonal = Math.Abs(columnDifference) == Math.Abs(rowDifference);
            if (!orthogonal && !diagonal)
            {
                throw new InvalidOperationException("Only orthogonal or diagonal lines are supported.");
            }
            Column column = from.Column;
            Row row = from.Row;
            int columnChange = Math.Sign(columnDifference);
            int rowChange = Math.Sign(rowDifference);
            while (true)
            {
                column += columnChange;
                row += rowChange;
                if (column == to.Column && row == to.Row)
                {
                    break;
                }
                yield return new Position(column, row);
            }
        }

        private class MockBoard : IReadOnlyBoard
        {
            private readonly Func<IEnumerable<Move>> history;
            private readonly Func<Column, Row, IReadOnlyTile> getTile;
            private readonly Func<PieceColor, IEnumerable<IReadOnlyTile>> tilesByColorOfPiece;

            public MockBoard(Func<IEnumerable<Move>> history, Func<Column, Row, IReadOnlyTile> getTile, Func<PieceColor, IEnumerable<IReadOnlyTile>> tilesByColorOfPiece)
            {
                this.history = history;
                this.getTile = getTile;
                this.tilesByColorOfPiece = tilesByColorOfPiece;
            }

            public IEnumerable<Move> History { get { return history(); } }

            public IReadOnlyTile GetTile(Position position) { return getTile(position.Column, position.Row); }
            public IReadOnlyTile GetTile(Column column, Row row) { return getTile(column, row); }
            public IEnumerable<IReadOnlyTile> TilesByColorOfPiece(PieceColor color) { return tilesByColorOfPiece(color); }
        }
    }
}
